﻿// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved
// Developer:  Michael Antonio
using System;
using System.Reflection;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Collections;
using MichMan.Utilities;

namespace MichMan.Utilities
{
    /// <summary>
    /// Specify the format for a URI.  Something like "{host}/api/v1.0/{fname}"
    /// You can specify query params in the format, or use the QueryParam attribute, or both.  
    /// Query string params specified with the QueryParam attribute go after the UriFormat.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
    public class UriFormatAttribute : Attribute
    {
        public UriFormatAttribute(string format)
        {
            Format = format;
            UriKind = System.UriKind.RelativeOrAbsolute;
        }

        public UriFormatAttribute(string format, UriKind uriKind)
        {
            Format = format;
            UriKind = uriKind;
        }

        public virtual string Format { get; set; }
        public virtual UriKind UriKind { get; set; }
    }

    /// <summary>
    /// The idea is to have an easy way to specify headers.  
    /// TODO:  Implement
    /// </summary>
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class HttpHeaderAttribute : FormattableAttribute
    {
        public HttpHeaderAttribute(string name)
        {
            Name = name;
        }
        public string Name { get; set; }
        public bool Required { get; set; }
    }

    /// <summary>
    /// A property that is used to fill in part of the UriFormat.
    /// </summary>
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class UriParamAttribute : FormattableAttribute
    {
        public UriParamAttribute(string name)
        {
            Name = name;
        }

        public string Name { get; set; }
    }

    /// <summary>
    /// A list of params all with the same name.  E.G. "id=foo&id=bar&id=baz"
    /// </summary>
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class QueryParamListAttribute : QueryParamAttribute
    {
        public QueryParamListAttribute(string name)
            : base(name)
        {
        }

        public override string ToString(object value)
        {
            if (value == null)
            {
                if (Required)
                {
                    // Cannot successfully construct the query parameter.
                    throw new InvalidOperationException(String.Format("Required parameter \"{0}\" missing.", Name));
                }
                return null;
            }

            List<object> values = new List<object>();
            values.AddRange(((IEnumerable)value).Cast<object>());

            StringBuilder builder = new StringBuilder();
            foreach (var v in values)
            {
                if (builder.Length != 0)
                {
                    builder.Append("&");
                }
                builder.Append(base.ToString(v));
            }
            return builder.ToString();
        }
    }

    /// <summary>
    /// Use this attribute to delcare query parameter properties on your request objects and they will be picked up automatically.
    /// </summary>
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class QueryParamAttribute : FormattableAttribute
    {
        public QueryParamAttribute(string name)
        {
            Name = name;
            Required = false;
            Order = int.MaxValue;
        }

        /// <summary>
        /// Query string parameter name.
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// Throws an InvalidOperationException if this is set and the property is null.
        /// </summary>
        public bool Required { get; set; }
        /// <summary>
        /// Allows you to optionally order parameters.
        /// </summary>
        public int Order { get; set; }

        /// <summary>
        /// Override this to do something like pairs of params or something more complex.
        /// See QueryParamList class for an example.
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public override string ToString(object value)
        {
            if (value == null)
            {
                if (Required)
                {
                    // Cannot successfully construct the query parameter.
                    throw new InvalidOperationException(String.Format("Required parameter \"{0}\" missing.", Name));
                }
                return null;
            }

            string strValue = base.ToString(value);

            if (String.IsNullOrEmpty(strValue) && Required)
            {
                // Cannot successfully construct the query parameters.
                throw new InvalidOperationException(String.Format("Required parameter \"{0}\" missing.", Name));
            }

            return String.Format("{0}={1}", Name, strValue);
        }
    }

    public class UriInferer
    {
        public UriInferer(object target)
        {
            Target = target;
        }

        public object Target { get; set; }

        public static Uri InferUri(object o)
        {
            return new UriInferer(o).BuildUri();
        }

        public static Dictionary<string, string> InferHeaders(object o)
        {
            return new UriInferer(o).BuildHeaders();
        }

        public Dictionary<string, string> BuildHeaders()
        {
            var headers = new Dictionary<string, string>();

            foreach (var prop in Target.GetType().GetProperties())
            {
                HttpHeaderAttribute httpHeaderAttribute = prop.GetCustomAttributes(typeof(HttpHeaderAttribute), true).Cast<HttpHeaderAttribute>().SingleOrDefault();
                if (httpHeaderAttribute == null)
                {
                    continue;
                }

                string strValue = null;
                // Note that this returns null or the value for nullable types.  No conversion necessary.
                object value = prop.GetValue(Target, null);

                if (value != null || httpHeaderAttribute.Required)
                {
                    strValue = httpHeaderAttribute.ToString(value);
                }

                if (strValue != null)
                {
                    headers.Add(httpHeaderAttribute.Name, strValue);
                }
            }

            return headers;
        }

        public Uri BuildUri()
        {
            return BuildQueryParameters(BuildPath());
        }

        public Uri BuildPath()
        {
            UriFormatAttribute uriFormatAttribute = Target.GetType().GetTypeInfo().GetCustomAttributes(typeof(UriFormatAttribute), true).Cast<UriFormatAttribute>().SingleOrDefault();

            if (uriFormatAttribute == null)
            {
                return null;
            }

            string format = uriFormatAttribute.Format;
            foreach (var prop in Target.GetType().GetProperties())
            {
                UriParamAttribute uriParamAttribute = prop.GetCustomAttributes(typeof(UriParamAttribute), true).Cast<UriParamAttribute>().SingleOrDefault();
                if (uriParamAttribute == null)
                {
                    continue;
                }

                string strValue = null;
                // Note that this returns null or the value for nullable types.  No conversion necessary.
                object value = prop.GetValue(Target, null);

                if (value == null)
                {
                    strValue = string.Empty;
                }
                else
                {
                    strValue = uriParamAttribute.ToString(value);
                }

                format = format.Replace("{" + uriParamAttribute.Name + "}", strValue);
            }

            if (String.IsNullOrEmpty(format))
            {
                return null;
            }

            return new Uri(format, uriFormatAttribute.UriKind);
        }

        private class QueryParam
        {
            public string Param{ get; set; }
            public int Order { get; set; }
        }

        /// <summary>
        /// If any properties are annotated with the QueryParam attribute, use them here.
        /// </summary>
        public Uri BuildQueryParameters(Uri uri)
        {
            if (uri == null)
            {
                return null;
            }

            UriFormatAttribute uriFormatAttribute = (UriFormatAttribute)Target.GetType().GetTypeInfo().GetCustomAttributes(typeof(UriFormatAttribute), true).SingleOrDefault();

            List<QueryParam> qParams = new List<QueryParam>();
            foreach (var prop in Target.GetType().GetProperties())
            {
                QueryParamAttribute queryParamAttribute = (QueryParamAttribute)prop.GetCustomAttributes(true).SingleOrDefault(a => typeof(QueryParamAttribute).GetTypeInfo().IsAssignableFrom(a.GetType().GetTypeInfo()));
                if (queryParamAttribute == null)
                {
                    continue;
                }

                // Note that this returns null or the value for nullable types.  No conversion necessary.
                object value = prop.GetValue(Target, null);
                string strValue = queryParamAttribute.ToString(value);

                if (strValue == null)
                {
                    continue;
                }

                QueryParam qp = new QueryParam()
                {
                    Param = strValue,
                    Order = queryParamAttribute.Order,
                };
                qParams.Add(qp);
            }

            // Now we have all the annotated query parameters.
            if (qParams.Count == 0)
            {
                return uri;
            }

            // Sort the list by order
            qParams = qParams.OrderBy(qp => qp.Order).ToList();

            StringBuilder queryString = new StringBuilder();
            queryString.Append(uri.ToString().Contains("?") ? "&" : "?");

            foreach (var param in qParams)
            {
                if (queryString.Length > 2)
                {
                    queryString.Append("&");
                }
                queryString.Append(param.Param);
            }

            UriKind kind;
            if (uriFormatAttribute != null)
            {
                kind = uriFormatAttribute.UriKind;
            }
            else
            {
                kind = uri.IsAbsoluteUri ? UriKind.Absolute : UriKind.Relative;
            }

            return new Uri( uri.ToString() + queryString, kind);
        }
    }
}